联盟推广者查询接口 API 文档

📋 接口概述

接口功能:获取当前策展人(Curator)的所有通过联盟邀请链接注册的推广者列表

业务场景:策展人查看自己邀请的联盟推广者信息,支持按关键字搜索(姓名、邮箱、手机号、店铺名等)

核心特性

  • 仅返回通过 INVITATION_LINK 类型邀请的推广者关联
  • 支持可选的关键字模糊搜索
  • 自动过滤已删除的关联记录(deletedAt IS NULL
  • 包含策展人的子域名信息用于构建推广链接

🔗 接口地址

GET /promoter-association/affiliate-promoters

环境

  • 生产环境: https://katana-api.1m.app
  • 测试环境: https://staging.katana-api.1m.app

📝 请求头(Headers)

Header 名称 是否必填 类型 说明 示例值
Authorization ✅ 是 string JWT 认证令牌 Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
from ✅ 是 string 客户端标识 client
timezone string 时区信息 Asia/Shanghai
x-track-id string 请求追踪ID 316d56b9-7ccf-4f84-a79d-1c90fc31fc89

[!note] 认证说明 该接口需要 JWT Token 认证,用户角色必须是 BUSINESS_PARTNER 或具有策展人权限。


📥 请求参数(Query Parameters)

参数名 是否必填 类型 说明 示例值
keyword ❌ 否 string 搜索关键字,支持模糊匹配 123

关键字搜索范围

  • 用户姓名(first_name + last_name
  • 邮箱地址(email
  • 手机号码(phone_number
  • 个性化链接(vanity_url
  • 店铺名称(store_name

[!tip] 搜索行为

  • 不提供 keyword 时,返回所有关联的推广者信息
  • 提供 keyword 时,仅在关联的推广者中进行模糊搜索
  • 搜索使用 PostgreSQL 的 ILIKE 进行大小写不敏感匹配

📤 响应结构

响应类型

HTTP 状态码: 200 OK

响应格式: application/json

响应内容: QueryAffiliateLinkResponse[] 数组

响应字段说明

字段名 类型 是否必返 说明
id string ✅ 是 推广者用户 ID(UUID)
curatorId string ✅ 是 策展人用户 ID(当前用户 ID)
promoterFullName string ✅ 是 推广者全名(优先使用姓名,否则邮箱/手机号)
vanityUrl string \ null ✅ 是 推广者个性化链接
logo string \ null ✅ 是 推广者头像 URL
affiliateCode string ✅ 是 推广者联盟代码
subDomainUrl string ✅ 是 策展人子域名 URL(无子域名时为空字符串)
invitationType string ✅ 是 邀请类型(固定为 INVITATION_LINK

TypeScript 类型定义

interface QueryAffiliateLinkResponse {
  id: string;                  // 推广者用户 ID
  curatorId: string;           // 策展人用户 ID
  promoterFullName: string;    // 推广者全名
  vanityUrl: string | null;    // 个性化链接
  logo: string | null;         // 头像 URL
  affiliateCode: string;       // 联盟代码
  subDomainUrl: string;        // 策展人子域名
  invitationType: string;      // 邀请类型
}

interface QueryAffiliateLinkRequest {
  keyword?: string;  // 可选搜索关键字
}

💡 成功响应示例

请求示例

curl -X GET 'https://staging.katana-api.1m.app/promoter-association/affiliate-promoters?keyword=123' \
  -H 'accept: application/json, text/plain, */*' \
  -H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
  -H 'from: client' \
  -H 'timezone: Asia/Shanghai'

响应示例(200 OK)

[
  {
    "id": "e1bf696f-5040-4801-a04c-52e7801eea9f",
    "curatorId": "550e8400-e29b-41d4-a716-446655440000",
    "promoterFullName": "张三",
    "vanityUrl": "zhangsan-shop",
    "logo": "https://cdn.example.com/avatars/zhangsan.jpg",
    "affiliateCode": "AFF123456",
    "subDomainUrl": "https://zhangsan.pear.us",
    "invitationType": "INVITATION_LINK"
  },
  {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "curatorId": "550e8400-e29b-41d4-a716-446655440000",
    "promoterFullName": "lisi@example.com",
    "vanityUrl": null,
    "logo": null,
    "affiliateCode": "AFF789012",
    "subDomainUrl": "https://zhangsan.pear.us",
    "invitationType": "INVITATION_LINK"
  }
]

空结果示例

[]

⚠️ 错误响应示例

401 Unauthorized - 未认证

{
  "statusCode": 401,
  "message": "Unauthorized",
  "error": "Unauthorized"
}

原因: JWT Token 无效、过期或未提供

403 Forbidden - 无权限

{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

原因: 用户角色不是 BUSINESS_PARTNER 或无策展人权限

404 Not Found - 用户不存在

{
  "statusCode": 404,
  "message": "Resource not found",
  "error": "Not Found",
  "curatorId": "550e8400-e29b-41d4-a716-446655440000"
}

原因: 当前用户在数据库中不存在(已被删除)


🔄 业务流程

泳道图

sequenceDiagram
    autonumber
    participant Client as 客户端
    participant Controller as PromoterAssociationController
    participant Service as PromoterAssociationService
    participant Repo as PromoterAssociationRepository
    participant UserRepo as UserMetaRepository
    participant DB as PostgreSQL Database

    Client->>Controller: GET /affiliate-promoters?keyword=xxx
    activate Controller

    Controller->>Controller: 从 JWT 获取 curatorId
    Controller->>UserRepo: findUserById(curatorId)
    UserRepo-->>Controller: User | null

    alt 用户不存在
        Controller-->>Client: 404 Not Found
    end

    par 并行查询
        Controller->>Service: findPromoterAssociationsByLinkType()
        Service->>Repo: findMany(where)
        Repo->>DB: SELECT * FROM PromoterAssociation<br/>WHERE curatorId=? AND invitationType='INVITATION_LINK'<br/>AND deletedAt IS NULL
        DB-->>Repo: PromoterAssociation[]
        Repo-->>Service: PromoterAssociation[]
        Service-->>Controller: PromoterAssociation[]
    and
        Controller->>Repo: getSubdomainByUserId(curatorId)
        Repo->>DB: SELECT * FROM SubDomain<br/>WHERE userId=?
        DB-->>Repo: SubDomain[]
        Repo-->>Controller: SubDomain[]
    end

    Controller->>Controller: 提取 promoterIds(去重)

    alt 存在 keyword
        Controller->>Service: searchAffiliateLinkUsersInfo(userIds, keyword)
        Service->>DB: $queryRaw(ILIKE 多字段搜索)
        DB-->>Service: QueryAffiliateLinkUserInfo[]
    else 无 keyword
        Controller->>Service: findAffiliateLinkUsersInfo(userIds)
        Service->>DB: SELECT * FROM User WHERE id IN (?)
        DB-->>Service: User[]
        Service->>Service: 转换为 UserEntity 并映射
        Service-->>Controller: QueryAffiliateLinkUserInfo[]
    end

    Controller->>Controller: 构建 promoterUserMap<br/>过滤并组装响应数据
    Controller-->>Client: 200 OK + QueryAffiliateLinkResponse[]
    deactivate Controller

数据流图

graph TD
    A[客户端请求] --> B[解析 JWT 获取 curatorId]
    B --> C{验证策展人存在?}
    C -->|否| D[返回 404]
    C -->|是| E[并行查询]

    E --> F[查询 PromoterAssociation]
    E --> G[查询 SubDomain]

    F --> H[提取 promoterIds 去重]
    G --> I[获取子域名 URL]

    H --> J{有 keyword?}
    J -->|是| K[执行 ILIKE 模糊搜索]
    J -->|否| L[批量查询用户信息]

    K --> M[构建用户信息映射]
    L --> M

    M --> N[组装响应数据]
    I --> N

    N --> O[返回响应数组]

🗄️ 数据库操作

涉及数据表

1. PromoterAssociation 表

查询条件:

SELECT *
FROM "PromoterAssociation"
WHERE "curatorId" = $1
  AND "invitationType" = 'INVITATION_LINK'
  AND "deletedAt" IS NULL
ORDER BY "createdAt" DESC;

索引建议:

  • 复合索引: (curatorId, invitationType, deletedAt)

2. User 表

无关键字时:

SELECT *
FROM "User"
WHERE "id" IN ($1, $2, ...);

有关键字时(使用原始 SQL):

SELECT u.id, u.first_name, u.last_name, u.email,
       u.phone_number, u.vanity_url, u.affiliate_code,
       u.logo, u.store_name
FROM "User" u
WHERE u.id IN ($1, $2, ...)
  AND (
    (u.first_name || ' ' || u.last_name) ILIKE $keyword
    OR u.email ILIKE $keyword
    OR u.phone_number ILIKE $keyword
    OR u.vanity_url ILIKE $keyword
    OR u.store_name ILIKE $keyword
  );

3. SubDomain 表

SELECT *
FROM "SubDomain"
WHERE "userId" = $1
ORDER BY "createdAt" DESC;

数据表关系

erDiagram
    PromoterAssociation ||--o{ User : "promoterId"
    PromoterAssociation }o--|| User : "curatorId"
    User ||--o{ SubDomain : "userId"

    PromoterAssociation {
        uuid id PK
        uuid curatorId FK
        uuid promoterId FK
        string invitationType
        timestamp deletedAt
        timestamp createdAt
    }

    User {
        uuid id PK
        string first_name
        string last_name
        string email
        string phone_number
        string vanity_url
        string affiliate_code
        string logo
        string store_name
    }

    SubDomain {
        uuid id PK
        uuid userId FK
        string subDomainUrl
        timestamp createdAt
    }

⚙️ 业务逻辑说明

1. 用户验证阶段

  1. 从 JWT Token 中提取 curatorId(当前用户 ID)
  2. 调用 UserMetaRepository.findUserById() 验证用户存在
  3. 用户不存在时抛出 ResourceNotFound 异常

2. 数据查询阶段

并行查询(使用 Promise.all):

  • PromoterAssociation: 查询所有 INVITATION_LINK 类型的关联记录
    • 筛选条件: curatorId, invitationType = INVITATION_LINK, deletedAt = null
    • 排序: 按 createdAt DESC
  • SubDomain: 查询策展人的子域名记录
    • 取最新的子域名(按 createdAt DESC

3. 用户信息获取阶段

根据有无关键字选择查询方式:

场景 查询方式 特点
无关键字 findUsersByIds() 批量查询 User 表,性能较高
有关键字 searchAffiliateLinkUsersInfo() 使用 $queryRaw 执行 ILIKE 多字段搜索

4. 数据组装阶段

  1. 去重: 使用 Array.from(new Set(promoterIds)) 去除重复的推广者 ID
  2. 构建映射: 使用 keyBy() 将用户信息转换为 id 为键的 Map
  3. 过滤: 遍历关联记录,跳过用户信息不存在的记录
  4. 组装: 构建 QueryAffiliateLinkResponse 对象

5. 全名处理逻辑

fullName =
  userEntity.fullName !== '' ? userEntity.fullName :
  userEntity.email !== '' ? userEntity.email :
  userEntity.phoneNumber

优先级: 全名 > 邮箱 > 手机号


📊 注意事项

1. 权限控制

  • 该接口仅限 BUSINESS_PARTNER 角色访问
  • 策展人只能查看自己邀请的推广者,无法查看其他策展人的数据

2. 邀请类型

  • 接口硬编码仅返回 INVITATION_LINK 类型的关联
  • 不会返回 AFFILIATE 类型或其他类型的推广者关联

3. 软删除处理

  • 已删除的关联记录(deletedAt IS NOT NULL)会被自动过滤
  • 符合平台数据保留策略

4. 空值处理

字段 空值处理
subDomainUrl 无子域名时返回空字符串(非 null)
vanityUrl 无个性化链接时返回 null
logo 无头像时返回 null

5. 性能优化建议

  • 并行查询: 策展人信息、关联记录、子域名使用 Promise.all 并行获取
  • 批量查询: 用户信息使用 IN 子句批量查询
  • 去重: 推广者 ID 使用 Set 去重,减少重复查询

6. 搜索限制

  • 搜索使用 ILIKE 进行模糊匹配
  • 关键字搜索仅在关联的推广者范围内进行,不是全局搜索
  • 前端建议添加防抖(debounce)处理,避免频繁请求

7. 数据一致性

  • 如果推广者在关联创建后被删除,该记录会被静默过滤(不抛出异常)
  • 返回结果可能少于 PromoterAssociation 表中的记录数

🔗 相关接口

  • [[kat-KAT-10550-curator-affiliate-link-create-20260226|创建联盟推广链接]]
  • [[kat-short-link-internal-create-20260227|短链接生成接口]]

📝 更新日志

版本 日期 作者 变更说明
1.0.0 2026-02-27 Claude 初始版本

🏷️ 标签

promoter-association #affiliate #api #curator #联盟营销

results matching ""

    No results matching ""